package io.shuttle.mbe.core

import android.net.Uri
import android.os.Build
import android.webkit.JavascriptInterface
import android.webkit.ValueCallback
import android.webkit.WebView
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.core.net.toUri
import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.WebMessageCompat
import androidx.webkit.WebMessagePortCompat
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import java.lang.ref.WeakReference
import kotlin.collections.get

/**
 * TODO: 1、freeze injected js, cannot be modified by webview 2、support wasm websocket etc.
 *
 */
interface WabChannel {
    /**
     * TODO: API check, WebViewFeature.isFeatureSupported may throw RuntimeException
     * TODO: inject time, usually after onPageFinished Event.
     */
    fun onStart(allowedOriginRules: Set<String>? = mutableSetOf())
    fun onDestroy()
    fun invokeJs(
        message: WebMessageCompat,
        legacyReply: ValueCallback<String>? = null,
        targetOrigin: Uri? = "*".toUri(),
    )

    fun invokeJs(
        replyBody: ReplyBody,
        legacyReply: ValueCallback<String>? = null,
        targetOrigin: Uri? = "*".toUri(),
    )
}

/**
 * 统一处理 js 传入的消息抽象层
 */
abstract class WabChannelImpl(
    val wv: WeakReference<WebView>,
    private val abilityMap: MutableMap<String, Ability>,
) : WabChannel {
    abstract val isLegacyWabChannel: Boolean
    abstract val isWebListenerWabChannel: Boolean
    abstract val isWebChannelWabChannel: Boolean

    /**
     * 解析 js 传入的 string 消息为 PostMessageBody
     * TODO: 实现 arraybuffer 类消息支持
     */
    private fun parseMessage(message: WebMessageCompat): PostMessageBody? {
        if (message.type == WebMessageCompat.TYPE_STRING) {
            if (message.data.isNullOrBlank()) {
                Logger.w("[WabChannelImpl] parseMessage illegal arguments from js!")
                invokeJs(replyBody = ReplyBody(error = Exception("[native.reply] js parameter  is null or empty!")))
                return null
            }
            val postMessageBody = PostMessageBody.fromJson(message.data!!)
            if (postMessageBody == null) {
                Logger.w("[WabChannelImpl] parseMessage parse failed!")
                invokeJs(replyBody = ReplyBody(error = Exception("[native.reply] parse js parameter to ReplyBody failed!")))
                return null
            }
            return postMessageBody
        } else {
            // TODO: arraybuffer support!
            return null
        }

    }

    /**
     * 处理 js 调用 native 函数 或者获取 native value
     */
    fun tryDispatch(message: WebMessageCompat?): ReplyBody? {
        if (message == null) return null
        val postMessageBody = parseMessage(message)
        if (postMessageBody == null) return null
        if (postMessageBody.type == PostMessageType.Function) {
            dispatchFunction(message, postMessageBody)
        } else if (postMessageBody.type == PostMessageType.Value) {
            return dispatchValue(postMessageBody)
        } else {
            Logger.e("[WabChannelImpl] tryDispatch unknown type!")
        }
        return null
    }

    /**
     * 处理 js 获取 native value
     */
    private fun dispatchValue(postMessageBody: PostMessageBody): ReplyBody? {
        // TODO: error
        val rb = ReplyBody(
            replyId = postMessageBody.replyId,
            replyOnce = postMessageBody.replyOnce,
            data = if (abilityMap.containsKey(postMessageBody.name))
                if (abilityMap[postMessageBody.name] is Ability.Value)
                    (abilityMap[postMessageBody.name] as Ability.Value).data
                else null
            else null
        )
        if (isLegacyWabChannel || isWebListenerWabChannel) {
            return if (rb.data == null) null else rb
        }
        invokeJs(rb)
        return null
    }

    /**
     * 处理 js 调用 native 函数, 如果 js 侧提供  replyId, 则 native 函数执行后会回调 js callback
     */
    private fun dispatchFunction(message: WebMessageCompat, postMessageBody: PostMessageBody) {
        if (postMessageBody.name == "onError") {
            // FIXME: args value maybe object.
            val error = postMessageBody.args?.get("error");
            Logger.e(error?.toString() ?: "unknown error")
            return
        }
        val ability = abilityMap[postMessageBody.name]
        if (ability == null || ability !is Ability.Function) {
            Logger.w("[WabChannelImpl] tryDispatch ability not found or ability is Value!")
            invokeJs(WebMessageCompat(ReplyBody().toString()))
            return
        }
        val sourceOrigin = postMessageBody.args?.get("sourceOrigin")?.toUri()
        ability.handle(
            message,
            postMessageBody,
            sourceOrigin,
            true,
            if (postMessageBody.replyId != null) {
                object : WabMessageReply {
                    override fun replyJs(
                        replyBody: ReplyBody,
                        wabSupportType: WabSupportType
                    ) {
                        replyBody.replyId = postMessageBody.replyId
                        replyBody.replyOnce = postMessageBody.replyOnce
                        invokeJs(
                            WebMessageCompat(replyBody.toString()),
                            targetOrigin = sourceOrigin,
                        )
                    }
                }
            } else {
                null
            }
        )
        return
    }
}

/**
 * @JavascriptInterface 交互
 */
class LegacyWabChannel(
    wv: WeakReference<WebView>,
    abilityMap: MutableMap<String, Ability>,
    override val isLegacyWabChannel: Boolean = true,
    override val isWebListenerWabChannel: Boolean = false,
    override val isWebChannelWabChannel: Boolean = false,
) : WabChannelImpl(wv, abilityMap) {

    override fun onStart(allowedOriginRules: Set<String>?) {
        wv.get()?.addJavascriptInterface(this, Wab.LINK_LEGACY)
    }

    override fun onDestroy() {
        wv.get()?.removeJavascriptInterface(Wab.LINK_LEGACY)
    }

    override fun invokeJs(
        message: WebMessageCompat,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        if (message.data != null) {
            val json = mutableMapOf(
                "data" to message.data
            ).toJSObject()?.toString()
            wv.get()?.also { web ->
                web.post {
                    web.evaluateJavascript("window.Wab.onMessage(${json})", legacyReply)
                }
            }
        }
    }

    override fun invokeJs(
        replyBody: ReplyBody,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        this.invokeJs(WebMessageCompat(replyBody.toString()), legacyReply, targetOrigin)
    }

    /**
     * 接收的参数和返回值类型
     *  - primitive values 原生值
     *  - single-dimentional arrays 一维数组
     *  - array-like JavaScript objects(processing "length" property, and also typed arrays from ES6)
     *  - previously injected java objects(from JS to Java) 先前注入到 JS 的对象,又从 JS 传了回来
     *  - new Java objects(from Java to JS), those are “injected” into JavaScript as if one called addJavascriptInterface, but w/o providing a name; also, the lifecycle of such transient objects is different (see below).
     *      - 新的从 Java 注入到 JS 的对象.
     *  https://chromium.googlesource.com/chromium/src/+/master/android_webview/docs/java-bridge.md
     *  https://www.oracle.com/java/technologies/javase/liveconnect-docs.html
     */
    @JavascriptInterface
    fun postMessage(
        str: String,
    ): String? {
        return tryDispatch(WebMessageCompat(str)).toString()
    }
}

class WebListenerWabChannel(
    wv: WeakReference<WebView>,
    abilityMap: MutableMap<String, Ability>,
    var legacyWabChannel: LegacyWabChannel? = null,
    override val isLegacyWabChannel: Boolean = false,
    override val isWebListenerWabChannel: Boolean = true,
    override val isWebChannelWabChannel: Boolean = false
) : WabChannelImpl(wv, abilityMap) {
    private var replyProxy: JavaScriptReplyProxy? = null

    override fun onStart(allowedOriginRules: Set<String>?) {
        WabCompat.checkRuntime(
            Build.VERSION_CODES.LOLLIPOP_MR1,
            onProcess = {
                WabCompat.checkFeature(
                    WebViewFeature.WEB_MESSAGE_LISTENER,
                    onProcess = {
                        wv.get()?.also { web ->
                            // 先尝试移除旧的监听器
                            try {
                                WebViewCompat.removeWebMessageListener(web, Wab.LINK_LISTENER)
                            } catch (e: Exception) {
                                Logger.w("[WebListenerWabChannel.onStart] Failed to remove old listener: ${e.message}")
                            }
                            @Suppress("RequiresFeature")
                            WebViewCompat.addWebMessageListener(
                                web,
                                Wab.LINK_LISTENER,
                                mutableSetOf<String>("*")
                            ) { view, message, sourceOrigin, isMainFrame, replyProxy ->
                                this.replyProxy = replyProxy
                                val replyBody = tryDispatch(message)
                                if (replyBody != null) {
                                    replyProxy.postMessage(replyBody.toString())
                                }
                            }
                        }

                    },
                    onDeny = {
                        Logger.w("[WebListenerWabChannel.onStart] not support  ${WebViewFeature.WEB_MESSAGE_LISTENER} feature.")
                    })
            },
            onDeny = {
                Logger.w("[WebListenerWabChannel.onStart] api lower than  Build.VERSION_CODES.LOLLIPOP_MR1")
            })

    }

    override fun onDestroy() {
        WabCompat.checkFeature(
            WebViewFeature.WEB_MESSAGE_LISTENER,
            onProcess = {
                wv.get()?.apply {
                    @Suppress("RequiresFeature")
                    WebViewCompat.removeWebMessageListener(this, Wab.LINK_LISTENER)
                }
            }, onDeny = {
                Logger.w("[WebChannelWabChannel.onDestroy] not support  ${WebViewFeature.WEB_MESSAGE_LISTENER} feature.")
            })
    }

    override fun invokeJs(
        message: WebMessageCompat,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        @Suppress("SENSELESS_COMPARISON")
        WabCompat.checkFeature(
            WebViewFeature.WEB_MESSAGE_LISTENER,
            onProcess = {
                WabCompat.checkFeature(
                    WebViewFeature.POST_WEB_MESSAGE,
                    onProcess = {
                        // TODO:
                        wv.get()?.also { web ->
                            web.post {
                                @Suppress("RequiresFeature")
                                WebViewCompat.postWebMessage(
                                    web,
                                    message,
                                    targetOrigin ?: "*".toUri()
                                )
                            }
                        }

                    },
                    onDeny = {
                        Logger.w("[WebChannelWabChannel.invokeJs] not support  ${WebViewFeature.POST_WEB_MESSAGE} feature. turn to LegacyWabChannel.")
                        legacyWabChannel?.invokeJs(message, legacyReply, targetOrigin)
                    })
            },
            onDeny = {
                Logger.w("[WebChannelWabChannel.invokeJs] not support ${WebViewFeature.WEB_MESSAGE_LISTENER} feature. turn to LegacyWabChannel.")
                legacyWabChannel?.invokeJs(message, legacyReply, targetOrigin)
            })

    }

    override fun invokeJs(
        replyBody: ReplyBody,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        this.invokeJs(WebMessageCompat(replyBody.toString()), legacyReply, targetOrigin)
    }
}

class WebChannelWabChannel(
    wv: WeakReference<WebView>,
    abilityMap: MutableMap<String, Ability>,
    var legacyWabChannel: LegacyWabChannel? = null,
    override val isLegacyWabChannel: Boolean
    = false,
    override val isWebListenerWabChannel: Boolean = false,
    override val isWebChannelWabChannel: Boolean = true
) : WabChannelImpl(wv, abilityMap) {
    private var nativePort: WebMessagePortCompat? = null
    private var webPort: WebMessagePortCompat? = null

    override fun onStart(allowedOriginRules: Set<String>?) {
        WabCompat.checkRuntime(
            Build.VERSION_CODES.M,
            onProcess = {
                WabCompat.checkFeature(
                    WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL,
                    onProcess = {
                        @Suppress("RequiresFeature")
                        wv.get()?.also { web ->
                            val ports = WebViewCompat.createWebMessageChannel(web)
                            nativePort = ports[0]
                            webPort = ports[1]

                            WabCompat.checkFeature(
                                WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK,
                                onProcess = {
                                    @Suppress("RequiresFeature")
                                    nativePort?.setWebMessageCallback(object :
                                        WebMessagePortCompat.WebMessageCallbackCompat() {
                                        override fun onMessage(
                                            port: WebMessagePortCompat,
                                            message: WebMessageCompat?
                                        ) {
                                            val replyBody = tryDispatch(message)
                                            if (replyBody != null) {
                                                nativePort?.postMessage(
                                                    WebMessageCompat(
                                                        replyBody.toString()
                                                    )
                                                )
                                            }
                                        }
                                    })
                                },
                                onDeny = {
                                    Logger.w("[WebChannelWabChannel.invokeJs] not support ${WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK} feature.")
                                })
                            // register MessagePort to js
                            Logger.d("[WebChannelWabChannel.onStart] register MessagePort to js!")

                            wv.get()?.apply {
                                WebViewCompat.postWebMessage(
                                    this, WebMessageCompat("__setupPort__", arrayOf(webPort)),
                                    "*".toUri()
                                )
                            }
                        }

                    },
                    onDeny = {
                        // TODO: instead with web listener or legacy
                        Logger.w("[WebChannelWabChannel.invokeJs] not support ${WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL} feature.")
                    })
            },
            onDeny = {
                Logger.w("[WebListenerWabChannel.onStart] api lower than  Build.VERSION_CODES.LOLLIPOP_MR1")
            })
    }

    override fun onDestroy() {
        WabCompat.checkFeature(
            WebViewFeature.WEB_MESSAGE_PORT_CLOSE,
            onProcess = {
                @Suppress("RequiresFeature")
                nativePort?.close()
                @Suppress("RequiresFeature")
                webPort?.close()
            }, onDeny = {
                Logger.w("[WebChannelWabChannel.onDestroy] not support ${WebViewFeature.WEB_MESSAGE_PORT_CLOSE} feature.")
            })
    }

    override fun invokeJs(
        message: WebMessageCompat,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        WabCompat.checkFeature(
            WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE,
            onProcess = {
                nativePort?.postMessage(message)
            }, onDeny = {
                WabCompat.checkFeature(
                    WebViewFeature.POST_WEB_MESSAGE,
                    onProcess = {
                        wv.get()?.also { web ->
                            web.post {

                                @Suppress("RequiresFeature")
                                WebViewCompat.postWebMessage(
                                    web,
                                    message,
                                    targetOrigin ?: "*".toUri()
                                )
                            }
                        }
                    },
                    onDeny = {
                        // TODO: instead with web listener or legacy
                        Logger.w("[WebChannelWabChannel.invokeJs] not support ${WebViewFeature.POST_WEB_MESSAGE} feature. turn to LegacyWabChannel.")
                        legacyWabChannel?.invokeJs(message, legacyReply, targetOrigin)
                    })
                Logger.w("[WebChannelWabChannel.invokeJs] not support ${WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE} feature. turn to LegacyWabChannel.")
                legacyWabChannel?.invokeJs(message, legacyReply, targetOrigin)
            })
    }

    override fun invokeJs(
        replyBody: ReplyBody,
        legacyReply: ValueCallback<String>?,
        targetOrigin: Uri?
    ) {
        this.invokeJs(WebMessageCompat(replyBody.toString()), legacyReply, targetOrigin)
    }
}

/**
 * compat with android api and webview feature
 */
class WabCompat {
    companion object {
        @ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
        fun checkRuntime(ver: Int, onProcess: () -> Unit, onDeny: (() -> Unit)? = null) {
            if (Build.VERSION.SDK_INT >= ver) {
                onProcess()
            } else {
                onDeny?.invoke()
                Logger.w("[📞🏄] SDK Version ${Build.VERSION.SDK_INT} less than $ver")
            }
        }

        fun checkFeature(
            feature: String,
            onProcess: () -> Unit,
            onDeny: (() -> Unit)? = null
        ) {
            if (WebViewFeature.isFeatureSupported(feature)) {
                onProcess()
            } else {
                onDeny?.invoke()
                Logger.w("[📞🏄] WebViewFeature.$feature is not supported")
            }
        }

        fun checkFeatures(
            features: Set<String>,
            onProcess: () -> Unit,
            onDeny: (() -> Unit)? = null
        ) {
            if (features.map { WebViewFeature.isFeatureSupported(it) }.all { it }) {
                onProcess()
            } else {
                onDeny?.invoke()
                Logger.w(
                    "[📞🏄] WebViewFeature.${
                        features.filter {
                            !WebViewFeature.isFeatureSupported(
                                it
                            )
                        }.joinToString(",")
                    } is not supported"
                )
            }
        }

        val isSupportWebChannel: Boolean
            get() {
                return WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) &&
                        WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)
            }
        val isSupportWebListener: Boolean
            get() {
                return WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)
                        && WebViewFeature.isFeatureSupported(
                    WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK
                ) && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)
                        && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)
                        && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)
            }
    }
}
